package com.code.man;

import com.code.man.config.GeneratorConfig;
import com.code.man.exception.RunException;
import com.code.man.model.ColumnInfo;
import com.code.man.model.TableInfo;
import com.code.man.util.FreeMarkerExtUtils;
import com.code.man.util.FreeMarkerUtils;
import com.code.man.util.PropertiesUtils;
import com.google.common.base.CaseFormat;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import org.apache.commons.lang3.StringUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;

/**
 * 代码生成工具
 *
 * @author devinpzhang
 * @date 2019年10月01日
 */
public class CodeGenerator {
    private static final String MATCH_PACKAGE = "{package}";
    private static final String CONFIG_XML = "code-man.xml";
    private static final String TEMPLATE_DIR = "code-template";
    private static final String EXT_JAVA = ".java";
    /**
     * 生成器配置
     */
    private GeneratorConfig config;
    /**
     * 项目目录
     **/
    private String projectDir;

    private static Map<String, String> dbType2JavaMap = new HashMap<>();

    static {
        dbType2JavaMap.put("BIGINT", "java.lang.Long");
        dbType2JavaMap.put("BIT", "java.lang.Integer");
        dbType2JavaMap.put("TINYINT", "java.lang.Integer");
        dbType2JavaMap.put("SMALLINT", "java.lang.Integer");
        dbType2JavaMap.put("MEDIUMINT", "java.lang.Integer");
        dbType2JavaMap.put("INTEGER", "java.lang.Integer");
        dbType2JavaMap.put("DECIMAL", "java.math.BigDecimal");
        dbType2JavaMap.put("DOUBLE", "java.math.BigDecimal");
        dbType2JavaMap.put("CHAR", "java.lang.String");
        dbType2JavaMap.put("VARCHAR", "java.lang.String");
        dbType2JavaMap.put("BLOB", "java.lang.String");
        dbType2JavaMap.put("TIMESTAMP", "java.util.Date");
        dbType2JavaMap.put("DATE", "java.util.Date");
        dbType2JavaMap.put("TIME", "java.util.Date");
        dbType2JavaMap.put("DATETIME", "java.util.Date");
    }

    public CodeGenerator(String projectDir) {
        if (StringUtils.isNotEmpty(projectDir) && !Paths.get(projectDir).toFile().exists()) {
            throw new RunException("项目目录错误：" + projectDir);
        }
        this.projectDir = projectDir;
    }

    public GeneratorConfig getConfig() {
        return this.config;
    }

    /**
     * 本地测试使用
     */
    public static void main(String[] args) {
        String baseDir = CodeGenerator.class.getClassLoader().getResource("").getPath();
        String projectDir = new File(baseDir).getAbsolutePath();

        CodeGenerator codeGenerator = new CodeGenerator(projectDir);
        codeGenerator.loadXmlConfig();

        codeGenerator.generate();
    }

    public void generate() {
        this.refreshConfig();

        List<TableInfo> tableInfoList = null;
        try (Connection connection = this.getConnection()) {
            tableInfoList = this.loadTableInfos(connection.getMetaData());
        } catch (SQLException e) {
            throw new RunException(e);
        }

        if (tableInfoList == null || tableInfoList.isEmpty()) {
            throw new RunException("未找到相关表");
        }

        // 生成代码
        for (TableInfo tableInfo : tableInfoList) {
            this.generateCode(tableInfo);
        }
    }

    /**
     * 创建数据库连接
     * @return
     * @throws SQLException
     */
    private Connection getConnection() throws SQLException {
        Properties info = new Properties();
        //设置可以获取remarks信息
        info.put("remarks", "true");
        info.put("useInformationSchema", "true");
        if (StringUtils.isNotBlank(config.getUsername())) {
            info.put("user", config.getUsername());
        }
        if (StringUtils.isNotBlank(config.getPassword())) {
            info.put("password", config.getPassword());
        }

        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        return DriverManager.getConnection(config.getJdbcUrl(), info);
    }


    /**
     * 加载配置信息
     */
    public void loadXmlConfig() {
        Path path = Paths.get(projectDir, CONFIG_XML);
        Document doc = parseDocument(path);
        if (doc == null) {
            throw new RunException("配置文件加载失败：" + CONFIG_XML);
        }

        config = new GeneratorConfig();

        //datasource
        Element element = (Element) doc.getElementsByTagName("datasource").item(0);
        config.setJdbcUrl(element.getElementsByTagName("jdbc-url").item(0).getTextContent());
        config.setUsername(element.getElementsByTagName("user").item(0).getTextContent());
        config.setPassword(element.getElementsByTagName("password").item(0).getTextContent());

        //type-mappers
        element = (Element) doc.getElementsByTagName("type-mappers").item(0);
        NodeList nodeList = element.getElementsByTagName("type-mapper");
        for (int i = 0; i < nodeList.getLength(); i++) {
            element = (Element)nodeList.item(i);
            dbType2JavaMap.put(element.getAttribute("dbType"), element.getAttribute("javaType"));
        }

        //generate
        element = (Element) doc.getElementsByTagName("generate").item(0);
        config.setAuthor(element.getElementsByTagName("author").item(0).getTextContent());
        config.setDatabase(element.getElementsByTagName("database").item(0).getTextContent());

        Element tablesElement = (Element) element.getElementsByTagName("tables").item(0);
        String tableNames = tablesElement.getElementsByTagName("names").item(0).getTextContent();
        config.setTables(Sets.newHashSet(tableNames.split(",")));
        config.setTablesIgnorePrefix(tablesElement.getElementsByTagName("ignore-prefix").item(0).getTextContent());

        config.setOutputPath(element.getElementsByTagName("output-path").item(0).getTextContent());
        config.setOutputJava(element.getElementsByTagName("output-java").item(0).getTextContent());
        config.setOutputXml(element.getElementsByTagName("output-xml").item(0).getTextContent());

        Element javaPkgElement = (Element) element.getElementsByTagName("java-package").item(0);
        config.setGeneratePackage(javaPkgElement.getAttribute("package"));
        config.setPackageEntity(javaPkgElement.getElementsByTagName("entity").item(0).getTextContent());
        config.setPackageMapper(javaPkgElement.getElementsByTagName("mapper").item(0).getTextContent());
        config.setPackageService(javaPkgElement.getElementsByTagName("service").item(0).getTextContent());
        config.setPackageServiceImpl(javaPkgElement.getElementsByTagName("serviceImpl").item(0).getTextContent());
        config.setPackageController(javaPkgElement.getElementsByTagName("controller").item(0).getTextContent());

        Element resPkgElement = (Element) element.getElementsByTagName("resources-package").item(0);
        config.setPackageMapperXml(resPkgElement.getElementsByTagName("mapper-xml").item(0).getTextContent());

        Element suffixElement = (Element) element.getElementsByTagName("suffix").item(0);
        config.setSuffixEntity(suffixElement.getElementsByTagName("entity").item(0).getTextContent());
        config.setSuffixMapper(suffixElement.getElementsByTagName("mapper").item(0).getTextContent());
        config.setSuffixService(suffixElement.getElementsByTagName("service").item(0).getTextContent());
        config.setSuffixServiceImpl(suffixElement.getElementsByTagName("serviceImpl").item(0).getTextContent());
        config.setSuffixController(suffixElement.getElementsByTagName("controller").item(0).getTextContent());
    }

    public void loadPropertiesConfig() {
        Path path = Paths.get(projectDir, CONFIG_XML);
        Properties prop = PropertiesUtils.loadProps(path.toString());

        config = new GeneratorConfig();

        config.setAuthor(prop.getProperty("generate.author"));

        config.setJdbcUrl(prop.getProperty("datasource.jdbc-url"));
        config.setUsername(prop.getProperty("datasource.user"));
        config.setPassword(prop.getProperty("datasource.password"));

        config.setDatabase(prop.getProperty("generate.database"));
        config.setTables(Sets.newHashSet(prop.getProperty("generate.tables", "").split(",")));
        config.setTablesIgnorePrefix(prop.getProperty("generate.tables.ignore-prefix"));

        config.setOutputPath(prop.getProperty("generate.output-path"));
        config.setGeneratePackage(prop.getProperty("generate.package","default"));

        config.setPackageEntity(prop.getProperty("generate.package.entity"));
        config.setPackageMapper(prop.getProperty("generate.package.mapper"));
        config.setPackageService(prop.getProperty("generate.package.service"));
        config.setPackageServiceImpl(prop.getProperty("generate.package.serviceImpl"));
        config.setPackageController(prop.getProperty("generate.package.controller"));

        config.setPackageMapperXml(prop.getProperty("generate.package.mapper-xml"));

        config.setSuffixEntity(prop.getProperty("generate.suffix.entity", ""));
        config.setSuffixMapper(prop.getProperty("generate.suffix.mapper", "Mapper"));
        config.setSuffixService(prop.getProperty("generate.suffix.service", "Service"));
        config.setSuffixServiceImpl(prop.getProperty("generate.suffix.serviceImpl", "ServiceImpl"));
        config.setSuffixController(prop.getProperty("generate.suffix.controller", "Controller"));
    }

    /**
     * 生成代码
     *
     * @param tableInfo 生成表信息
     */
    private void generateCode(TableInfo tableInfo) {
        FreeMarkerUtils freeMarkerUtils;

        Path path = Paths.get(projectDir + TEMPLATE_DIR);
        if (path.toFile().exists()) {
            //当前项目不存在模板目录，从目录文件加载模板
            freeMarkerUtils = new FreeMarkerUtils(path.toString());
        } else {
            //当前项目不存在模板目录，从class路径加载模板
            freeMarkerUtils = new FreeMarkerUtils(this.getClass(), "/" + TEMPLATE_DIR);
        }

        Map<String, Object> dataMap = new HashMap<>(2);
        dataMap.put("config", config);
        dataMap.put("tableInfo", tableInfo);
        dataMap.put("utils", new FreeMarkerExtUtils());

        //java代码文件路径
        String codePath = Paths.get(config.getOutputPath(),config.getOutputJava()).toString();

        String filePath;
        String fileName;

        //生成Entity文件
        if(config.isEntityChecked()) {
            filePath = Paths.get(codePath, StringUtils.replace(config.getPackageEntity(), ".", "/")).toString();
            fileName = tableInfo.getJavaName() + config.getSuffixEntity() + EXT_JAVA;
            freeMarkerUtils.generateFileByTemplate("entity.ftl", filePath, fileName, dataMap);
        }

        //生成Mapper文件
        if(config.isMapperChecked()) {
            filePath = Paths.get(codePath, StringUtils.replace(config.getPackageMapper(), ".", "/")).toString();
            fileName = tableInfo.getJavaName() + config.getSuffixMapper() + EXT_JAVA;
            freeMarkerUtils.generateFileByTemplate("mapper.ftl", filePath, fileName, dataMap);
        }

        //生成Service文件
        if(config.isServiceChecked()) {
            filePath = Paths.get(codePath, StringUtils.replace(config.getPackageService(), ".", "/")).toString();
            fileName = tableInfo.getJavaName() + config.getSuffixService() + EXT_JAVA;
            freeMarkerUtils.generateFileByTemplate("service.ftl", filePath, fileName, dataMap);
        }

        //生成ServiceImpl文件
        if(config.isServiceImplChecked()) {
            filePath = Paths.get(codePath, StringUtils.replace(config.getPackageServiceImpl(), ".", "/")).toString();
            fileName = tableInfo.getJavaName() + config.getSuffixServiceImpl() + EXT_JAVA;
            freeMarkerUtils.generateFileByTemplate("serviceImpl.ftl", filePath, fileName, dataMap);
        }

        //生成Controller文件
        if(config.isControllerChecked()) {
            filePath = Paths.get(codePath, StringUtils.replace(config.getPackageController(), ".", "/")).toString();
            fileName = tableInfo.getJavaName() + config.getSuffixController() + EXT_JAVA;
            freeMarkerUtils.generateFileByTemplate("controller.ftl", filePath, fileName, dataMap);
        }

        //xml映射文件路径
        if(config.isMapperXmlChecked()) {
            String xmlPath = Paths.get(config.getOutputPath(), config.getOutputXml()).toString();

            filePath = Paths.get(xmlPath, StringUtils.replace(config.getPackageMapperXml(), ".", "/")).toString();
            fileName = tableInfo.getJavaName() + config.getSuffixMapper() + ".xml";
            freeMarkerUtils.generateFileByTemplate("mapper.xml.ftl", filePath, fileName, dataMap);
        }
    }

    /**
     * 加载表结构定义信息
     *
     * @param metaData 数据库元数据
     * @return 表结构定义信息列表
     */
    private List<TableInfo> loadTableInfos(DatabaseMetaData metaData) throws SQLException {
        List<TableInfo> tableInfoList = new ArrayList<>();

        Set<String> tableNames = config.getTables();
        for (String tableName : tableNames) {
            ResultSet rs = metaData.getTables(config.getDatabase(), null, tableName, new String[]{"TABLE"});
            while (rs.next()) {
                TableInfo tableInfo = new TableInfo();
                tableInfo.setComment(rs.getString("REMARKS"));
                tableInfo.setDbName(rs.getString("TABLE_NAME"));
                tableInfo.setJavaName(CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, tableInfo.getDbName().replace(config.getTablesIgnorePrefix(), "")));

                this.fillTableColumns(metaData, tableInfo);
                this.fillPrimaryKeys(metaData, tableInfo);
                tableInfoList.add(tableInfo);
            }
        }

        return tableInfoList;
    }

    /**
     * 填充数据表列信息
     *
     * @param metaData 数据库元数据
     * @param tableInfo 数据表
     * @throws SQLException 异常信息
     */
    private void fillTableColumns(DatabaseMetaData metaData, TableInfo tableInfo) throws SQLException {
        tableInfo.setColumns(Lists.newArrayList());
        ResultSet rs = metaData.getColumns(config.getDatabase(), "%", tableInfo.getDbName(), "%");
        while (rs.next()) {
            ColumnInfo columnInfo = new ColumnInfo();
            // 获取字段名称
            columnInfo.setDbName(rs.getString("COLUMN_NAME"));
            // 获取字段类型
            columnInfo.setDbType(rs.getString("TYPE_NAME"));
            // 字段在数据库的注释
            columnInfo.setComment(rs.getString("REMARKS"));
            // 对应Java字段名称(驼峰命名格式)
            columnInfo.setJavaName(CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, columnInfo.getDbName()));

            //解决无符号数据类型问题
            if (columnInfo.getDbType().contains("UNSIGNED")) {
                columnInfo.setDbType(columnInfo.getDbType().replaceAll(" UNSIGNED", ""));
            }

            // DB字段类型映射到Java字段类型
            columnInfo.setJavaType(dbType2JavaMap.get(columnInfo.getDbType()));
            if (StringUtils.isEmpty(columnInfo.getJavaType())) {
                throw new NullPointerException("字段 " + columnInfo.getDbName() + ",数据库类型：" + columnInfo.getDbType() + ",没有找到对应的类型");
            }
            tableInfo.getColumns().add(columnInfo);
        }
    }

    /**
     * 填充主键名称和主键数据列信息
     *
     * @param metaData 数据库元数据
     * @param tableInfo 数据表
     * @throws SQLException 异常信息
     */
    private void fillPrimaryKeys(DatabaseMetaData metaData, TableInfo tableInfo) throws SQLException {
        //支持联合主键
        Map<String, String> pkMaps = Maps.newHashMap();

        ResultSet pkRs = metaData.getPrimaryKeys(config.getDatabase(), null, tableInfo.getDbName());
        while (pkRs.next()) {
            String keyName = pkRs.getString("PK_NAME");
            String colName = pkRs.getString("COLUMN_NAME");
            pkMaps.put(colName, keyName);
        }

        int cnt = pkMaps.size();
        String keyName = null;

        tableInfo.setPrimaryKeyColumns(Lists.newArrayList());
        for (Map.Entry<String, String> entry : pkMaps.entrySet()) {
            //确定主键名称
            if (cnt == 1) {
                keyName = entry.getKey();
            } else {
                keyName = entry.getValue();
            }

            //查找到属于主键的列信息，加入到主键列List
            Optional<ColumnInfo> optional = tableInfo.getColumns().stream()
                    .filter(m -> m.getDbName().equals(entry.getKey()))
                    .findFirst();

            optional.ifPresent(columnInfo -> tableInfo.getPrimaryKeyColumns().add(columnInfo));
        }

        tableInfo.setPrimaryKey(keyName);
    }

    private Document parseDocument(Path path) {
        try (InputStream is = new FileInputStream(path.toFile())){
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            return builder.parse(is);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private void refreshConfig() {
        //替换{package}字符串
        String packageStr = config.getGeneratePackage();
        config.setPackageEntity(StringUtils.replace(config.getPackageEntity(), MATCH_PACKAGE, packageStr));
        config.setPackageMapper(StringUtils.replace(config.getPackageMapper(), MATCH_PACKAGE, packageStr));
        config.setPackageMapperXml(StringUtils.replace(config.getPackageMapperXml(), MATCH_PACKAGE, packageStr));
        config.setPackageService(StringUtils.replace(config.getPackageService(), MATCH_PACKAGE, packageStr));
        config.setPackageServiceImpl(StringUtils.replace(config.getPackageServiceImpl(), MATCH_PACKAGE, packageStr));
        config.setPackageController(StringUtils.replace(config.getPackageController(), MATCH_PACKAGE, packageStr));

        //默认使用当前项目路径作为输出文件根路径
        if(StringUtils.isBlank(config.getOutputPath())){
            config.setOutputPath(this.projectDir);
        }
    }
}
